1use super::reach_set2::{ReachSet2Options, reach_set2_backward};
20use crate::copp::copp2::formulation::Topp2Problem;
21use crate::copp::{ApproxOrdering, approx_order};
22use crate::diag::{
23 CoppError, DebugVerboser, SilentVerboser, SummaryVerboser, TraceVerboser, Verboser, Verbosity,
24 format_duration_human,
25};
26use crate::math::numerical::{LpToleranceOptions, lp_1d};
27use core::f64;
28use itertools::izip;
29
30pub fn topp2_ra(problem: &Topp2Problem, options: &ReachSet2Options) -> Result<Vec<f64>, CoppError> {
49 match options.verbosity {
50 Verbosity::Silent => topp2_ra_core(problem, (options, SilentVerboser)),
51 Verbosity::Summary => topp2_ra_core(problem, (options, SummaryVerboser::new())),
52 Verbosity::Debug => topp2_ra_core(problem, (options, DebugVerboser::new())),
53 Verbosity::Trace => topp2_ra_core(problem, (options, TraceVerboser::new())),
54 }
55}
56
57#[cold]
77#[inline(never)]
78#[allow(clippy::too_many_arguments)]
79fn recover_forward_intersection(
80 rows: &[(f64, f64, f64)],
81 a_prev: f64,
82 prev_backward: (f64, f64),
83 curr_backward: (f64, f64),
84 prev_path_max: f64,
85 curr_path_max: f64,
86 options: &ReachSet2Options,
87) -> Option<(f64, f64, f64)> {
88 let feas_tol = options.lp_feas_tol;
89 if !a_prev.is_finite()
90 || !feas_tol.is_finite()
91 || feas_tol < 0.0
92 || !prev_backward.0.is_finite()
93 || !prev_backward.1.is_finite()
94 || curr_backward.0.is_nan()
95 || curr_backward.1.is_nan()
96 || prev_path_max.is_nan()
97 || curr_path_max.is_nan()
98 {
99 return None;
100 }
101
102 let inside = |value: f64, lower: f64, upper: f64| {
103 value.is_finite()
104 && !matches!(
105 approx_order(value, lower, options.a_cmp_abs_tol, options.a_cmp_rel_tol,),
106 ApproxOrdering::Less
107 )
108 && !matches!(
109 approx_order(value, upper, options.a_cmp_abs_tol, options.a_cmp_rel_tol,),
110 ApproxOrdering::Greater
111 )
112 };
113 let prev_lower = prev_backward.0.max(0.0);
114 let prev_upper = prev_backward.1.min(prev_path_max);
115 if !prev_lower.is_finite() || !prev_upper.is_finite() || !inside(a_prev, prev_lower, prev_upper)
116 {
117 return None;
118 }
119
120 let lower = curr_backward.0.max(0.0);
124 let upper = curr_backward.1.min(curr_path_max);
125 if !lower.is_finite() || !upper.is_finite() || lower > upper {
126 return None;
127 }
128
129 let certify = |a_curr: f64| {
130 if !a_curr.is_finite() || a_curr < lower || a_curr > upper {
131 return None;
132 }
133 let mut worst_normalized = 0.0_f64;
134 let mut worst_raw = 0.0_f64;
135 for &(coef_curr, coef_prev, rhs) in rows {
136 if !coef_curr.is_finite() || !coef_prev.is_finite() || !rhs.is_finite() {
137 return None;
138 }
139 let norm = coef_curr.hypot(coef_prev);
140 if norm == 0.0 {
141 if rhs < 0.0 {
143 return None;
144 }
145 continue;
146 }
147 if !norm.is_finite() {
148 return None;
149 }
150 let norm_inv = 1.0 / norm;
151 let raw_tol = feas_tol * rhs.abs().max(1.0);
152 if !raw_tol.is_finite() {
153 return None;
154 }
155 let normalized_residual =
156 (coef_curr * norm_inv) * a_curr + (coef_prev * norm_inv) * a_prev - rhs * norm_inv;
157 let raw_residual = coef_curr * a_curr + coef_prev * a_prev - rhs;
158 let raw_normalized_residual = raw_residual / norm;
159 if !normalized_residual.is_finite()
160 || !raw_residual.is_finite()
161 || !raw_normalized_residual.is_finite()
162 || normalized_residual > feas_tol
163 || raw_normalized_residual > feas_tol
164 || raw_residual > raw_tol
165 {
166 return None;
167 }
168 worst_normalized = worst_normalized
169 .max(normalized_residual)
170 .max(raw_normalized_residual);
171 worst_raw = worst_raw.max(raw_residual);
172 }
173 Some((a_curr, worst_normalized, worst_raw))
174 };
175
176 if let Some(certified) = certify(upper) {
179 return Some(certified);
180 }
181
182 let mut projected_lower = lower;
187 let mut projected_upper = upper;
188 for &(coef_curr, coef_prev, rhs) in rows {
189 let norm = coef_curr.hypot(coef_prev);
190 if norm == 0.0 {
191 if rhs < 0.0 {
192 return None;
193 }
194 continue;
195 }
196 if !norm.is_finite() {
197 return None;
198 }
199 let norm_inv = 1.0 / norm;
200 let raw_tol = feas_tol * rhs.abs().max(1.0);
201 if !raw_tol.is_finite() {
202 return None;
203 }
204 let normalized_tol = feas_tol.min(raw_tol * norm_inv);
205 let coef_curr = coef_curr * norm_inv;
206 let coef_prev = coef_prev * norm_inv;
207 let rhs = rhs * norm_inv + normalized_tol - coef_prev * a_prev;
208 if !rhs.is_finite() {
209 return None;
210 }
211 if coef_curr > 0.0 {
212 projected_upper = projected_upper.min(rhs / coef_curr);
213 } else if coef_curr < 0.0 {
214 projected_lower = projected_lower.max(rhs / coef_curr);
215 } else if rhs < 0.0 {
216 return None;
217 }
218 }
219 if !projected_lower.is_finite()
220 || !projected_upper.is_finite()
221 || projected_lower > projected_upper
222 {
223 return None;
224 }
225
226 let mut candidate = projected_upper;
232 for _ in 0..=4 {
233 if candidate < projected_lower {
234 break;
235 }
236 if let Some(certified) = certify(candidate) {
237 return Some(certified);
238 }
239 candidate = candidate.next_down();
240 }
241
242 let mut certified_lower_x = 0.5 * projected_lower + 0.5 * projected_upper;
243 let mut certified_lower = certify(certified_lower_x)?;
244 let mut rejected_upper = projected_upper;
245 for _ in 0..64 {
246 let midpoint = 0.5 * certified_lower_x + 0.5 * rejected_upper;
247 if midpoint == certified_lower_x || midpoint == rejected_upper {
248 break;
249 }
250 if let Some(certified) = certify(midpoint) {
251 certified_lower_x = midpoint;
252 certified_lower = certified;
253 } else {
254 rejected_upper = midpoint;
255 }
256 }
257 Some(certified_lower)
258}
259
260fn topp2_ra_core(
262 problem: &Topp2Problem,
263 options_verboser: (&ReachSet2Options, impl Verboser),
264) -> Result<Vec<f64>, CoppError> {
265 let (options, mut verboser) = options_verboser;
266 if verboser.is_enabled(Verbosity::Summary) {
267 verboser.record_start_time();
268 crate::verbosity_log!(
269 Verbosity::Summary,
270 "\ntopp2_ra started: {} <= idx_s <= {}, a_start = {}, a_final = {}.",
271 problem.idx_s_interval.0,
272 problem.idx_s_interval.1,
273 problem.a_boundary.0,
274 problem.a_boundary.1,
275 );
276 }
277
278 let reach_set = reach_set2_backward(problem, options).map_err(|e| {
280 if verboser.is_enabled(Verbosity::Debug) {
281 crate::verbosity_log!(Verbosity::Debug, "{e:?}");
282 } else if verboser.is_enabled(Verbosity::Summary) {
283 crate::verbosity_log!(
284 Verbosity::Summary,
285 "topp2_ra: failed while computing backward reachable set."
286 );
287 }
288 e
289 })?;
290 let a_max = &reach_set.a_max;
291 let a_min = &reach_set.a_min;
292
293 if verboser.is_enabled(Verbosity::Debug) {
295 crate::verbosity_log!(Verbosity::Debug, "Forward pass started.");
296 }
297
298 let (idx_s_start, idx_s_final) = problem.idx_s_interval;
299 let n = idx_s_final - idx_s_start;
300 let mut a = vec![0.0; n + 1];
301 let mut a_prev = problem.a_boundary.0;
302 *a.first_mut().unwrap() = a_prev;
303
304 let mut a_b = Vec::<(f64, f64, f64)>::with_capacity(2 * problem.constraints.acc_rows());
305 for (k, (a_curr, &a_max_curr_, &a_min_curr_)) in
306 izip!(a.iter_mut(), a_max, a_min).enumerate().skip(1)
307 {
308 let idx_s = idx_s_start + k;
309 if verboser.is_enabled(Verbosity::Trace) {
310 crate::verbosity_log!(
311 Verbosity::Trace,
312 "\tForward pass at k = {k} (idx_s = {idx_s}): backward interval {a_min_curr_} <= a[k] <= {a_max_curr_}, a_prev = {a_prev}."
313 );
314 }
315
316 a_b.clear();
317 problem
318 .constraints
319 .fill_acc_topp2::<true>(&mut a_b, idx_s - 1);
320 let (mut a_max_curr, mut a_min_curr) = lp_1d::<false>(
325 a_b.iter().map(|&coeffs| {
326 (coeffs.0, coeffs.2 - coeffs.1 * a_prev)
329 }),
330 &LpToleranceOptions::with_feas_tol(options.lp_feas_tol),
331 );
332
333 if verboser.is_enabled(Verbosity::Trace) {
334 crate::verbosity_log!(
335 Verbosity::Trace,
336 "\t\tForward LP result before clipping: {a_min_curr} <= a[k] <= {a_max_curr}."
337 );
338 }
339
340 let (a_max_fwd, a_min_fwd) = (a_max_curr, a_min_curr);
342 let forward_lp_failed = a_max_fwd.is_nan() || a_min_fwd.is_nan();
347 a_max_curr = a_max_curr.min(a_max_curr_);
348 a_min_curr = a_min_curr.max(a_min_curr_);
349 if verboser.is_enabled(Verbosity::Trace) {
350 crate::verbosity_log!(
351 Verbosity::Trace,
352 "\t\tAfter clipping with backward reachable set: {a_min_curr} <= a[k] <= {a_max_curr}."
353 );
354 }
355
356 if forward_lp_failed || a_max_curr < a_min_curr {
357 let recovered = if a_max_curr_.is_finite() && a_min_curr_.is_finite() {
358 recover_forward_intersection(
359 &a_b,
360 a_prev,
361 (a_min[k - 1], a_max[k - 1]),
362 (a_min_curr_, a_max_curr_),
363 problem.constraints.amax_unchecked(idx_s - 1),
364 problem.constraints.amax_unchecked(idx_s),
365 options,
366 )
367 } else {
368 None
369 };
370 if let Some((a_recovered, worst_normalized, worst_raw)) = recovered {
371 if verboser.is_enabled(Verbosity::Debug) {
372 crate::verbosity_log!(
373 Verbosity::Debug,
374 "Recovered the forward step at idx_s = {idx_s}: a[k] = {a_recovered}, max normalized violation = {worst_normalized:.6e}, max raw violation = {worst_raw:.6e}."
375 );
376 }
377 a_max_curr = a_recovered;
378 a_min_curr = a_recovered;
379 } else if !forward_lp_failed
380 && !matches!(
381 approx_order(
382 a_max_curr,
383 a_min_curr,
384 options
387 .a_cmp_abs_tol
388 .max(options.a_cmp_rel_tol * a_prev.abs()),
389 options.a_cmp_rel_tol,
390 ),
391 ApproxOrdering::Less
392 )
393 {
394 let a_degenerate = 0.5 * (a_max_curr + a_min_curr);
397 if verboser.is_enabled(Verbosity::Debug) {
398 crate::verbosity_log!(
399 Verbosity::Debug,
400 "The forward reachable interval at idx_s = {idx_s} is inverted by {:.3e} at scale {:.3e}; treated as the degenerate point a[k] = {a_degenerate}.",
401 a_min_curr - a_max_curr,
402 a_prev.abs()
403 );
404 }
405 a_max_curr = a_degenerate;
406 a_min_curr = a_degenerate;
407 } else {
408 let err = CoppError::Infeasible(
413 "topp2_ra".into(),
414 format!(
415 "The reachable set is empty at index {} during the forward pass where a_max = {}, a_min = {}",
416 idx_s_start + k,
417 a_max_curr,
418 a_min_curr
419 ),
420 );
421 if verboser.is_enabled(Verbosity::Debug) {
422 crate::verbosity_log!(Verbosity::Debug, "{err:?}");
423 } else if verboser.is_enabled(Verbosity::Summary) {
424 crate::verbosity_log!(
425 Verbosity::Summary,
426 "topp2_ra: the forward pass failed at index {idx_s} due to infeasibility."
427 );
428 }
429 return Err(err);
430 }
431 }
432
433 if a_max_curr.is_infinite() {
434 let err = CoppError::Unbounded(
435 "topp2_ra".into(),
436 format!(
437 "The reachable set is unbounded at index {} during the forward pass where a_max = {}",
438 idx_s_start + k,
439 a_max_curr
440 ),
441 );
442 if verboser.is_enabled(Verbosity::Debug) {
443 crate::verbosity_log!(Verbosity::Debug, "{err:?}");
444 } else if verboser.is_enabled(Verbosity::Summary) {
445 crate::verbosity_log!(
446 Verbosity::Summary,
447 "topp2_ra: the forward pass failed at index {idx_s} due to unboundedness."
448 );
449 }
450 return Err(err);
451 }
452
453 if verboser.is_enabled(Verbosity::Debug)
454 && matches!(
455 approx_order(
456 a_max_curr,
457 a_min_curr,
458 options.a_cmp_abs_tol,
459 options.a_cmp_rel_tol,
460 ),
461 ApproxOrdering::Equal
462 )
463 {
464 crate::verbosity_log!(
465 Verbosity::Debug,
466 "The one-step forward reachable set at k = {k} (idx_s = {idx_s}) is degenerate since a_max and a_min are approximately equal at {}.",
467 0.5 * (a_max_curr + a_min_curr)
468 );
469 }
470
471 a_prev = a_max_curr;
472 *a_curr = a_prev;
473
474 if verboser.is_enabled(Verbosity::Trace) {
475 crate::verbosity_log!(
476 Verbosity::Trace,
477 "\t\tSelected maximal feasible state: a[k] = {a_prev}."
478 );
479 }
480 }
481
482 if verboser.is_enabled(Verbosity::Summary) {
483 crate::verbosity_log!(
484 Verbosity::Summary,
485 "topp2_ra: total elapsed time = {}.\n",
486 format_duration_human(verboser.elapsed())
487 );
488 }
489
490 Ok(a)
491}
492
493#[cfg(test)]
494mod tests {
495 use super::*;
496 use crate::copp::InterpolationMode;
497 use crate::copp::constraints::Constraints;
498 use crate::copp::copp2::stable::basic::{
499 Topp2ProblemBuilder, a_to_b_topp2, s_to_t_topp2, t_to_s_topp2,
500 };
501 use crate::copp::copp2::stable::reach_set2::ReachSet2OptionsBuilder;
502 use crate::path::{
503 Path, SplineConfig, add_symmetric_axial_limits_for_test, lissajous_path_for_test,
504 };
505 use crate::robot::robot_core::Robot;
506 use nalgebra::DMatrix;
507 use std::time::{Duration, Instant};
508
509 #[test]
510 fn test_topp2_ra() -> Result<(), CoppError> {
511 run_test_topp2_ra_repeated(1, false)
512 }
513
514 #[test]
515 fn test_recovery_numeric_contract_at_extremes() {
516 let options = ReachSet2OptionsBuilder::new().build().unwrap();
517
518 assert!(
519 recover_forward_intersection(
520 &[],
521 0.0,
522 (0.0, 0.0),
523 (0.0, 1.0),
524 f64::NEG_INFINITY,
525 1.0,
526 &options,
527 )
528 .is_none(),
529 "a non-finite effective predecessor interval must be rejected"
530 );
531
532 assert!(
533 recover_forward_intersection(
534 &[(0.0, 1.0, 1.0)],
535 2.0,
536 (0.0, 2.0),
537 (0.0, 2.0),
538 2.0,
539 2.0,
540 &options,
541 )
542 .is_none(),
543 "a violation independent of the current state must not be swallowed"
544 );
545
546 let folded_delta = 0.25 * options.a_cmp_abs_tol;
547 assert!(
548 recover_forward_intersection(
549 &[],
550 1.0,
551 (1.0 + folded_delta, 1.0 - folded_delta),
552 (0.0, 1.0),
553 2.0,
554 1.0,
555 &options,
556 )
557 .is_some(),
558 "a predecessor interval folded within a_cmp_tol remains admissible"
559 );
560
561 let (a_curr, normalized, _) = recover_forward_intersection(
564 &[(f64::MIN_POSITIVE, 0.0, 0.0)],
565 0.0,
566 (0.0, 0.0),
567 (0.0, 2.0),
568 2.0,
569 2.0,
570 &options,
571 )
572 .expect("stable row normalization must retain a finite subnormal-scale row");
573 assert!(a_curr <= options.lp_feas_tol);
574 assert!(normalized <= options.lp_feas_tol);
575
576 let (a_curr, normalized, raw) = recover_forward_intersection(
579 &[(1.0e12, 0.0, 1.0)],
580 0.0,
581 (0.0, 0.0),
582 (0.0, 1.0),
583 1.0,
584 1.0,
585 &options,
586 )
587 .expect("the physical residual cap still leaves a nonempty interval");
588 assert!(a_curr <= (1.0 + options.lp_feas_tol) / 1.0e12);
589 assert!(normalized <= options.lp_feas_tol);
590 assert!(raw <= options.lp_feas_tol);
591 }
592
593 fn constraints_with_first_edge_rows(
594 rows: &[(f64, f64, f64)],
595 ds: f64,
596 path_cap: f64,
597 ) -> Constraints {
598 let mut constraints = Constraints::with_capacity(1, 3);
599 constraints.with_s(&[0.0, ds, 2.0 * ds][..]).unwrap();
600 constraints
601 .with_constraint_1order(&[path_cap, path_cap, path_cap][..], 0)
602 .unwrap();
603
604 let acc_a = DMatrix::from_iterator(rows.len(), 1, rows.iter().map(|r| r.0 + r.1));
609 let acc_b = DMatrix::from_iterator(rows.len(), 1, rows.iter().map(|r| 2.0 * ds * r.0));
610 let acc_max = DMatrix::from_iterator(rows.len(), 1, rows.iter().map(|r| r.2));
611 constraints
612 .with_constraint_2order(
613 &acc_a.as_view(),
614 &acc_b.as_view(),
615 &acc_max.as_view(),
616 0,
617 false,
618 )
619 .unwrap();
620 constraints
621 }
622
623 fn first_edge_row_after_round_trip(
624 (coef_curr, coef_prev, rhs): (f64, f64, f64),
625 ds: f64,
626 ) -> (f64, f64, f64) {
627 let acc_a = coef_curr + coef_prev;
628 let acc_b = 2.0 * ds * coef_curr;
629 let coef_curr = acc_b / (2.0 * ds);
630 (coef_curr, acc_a - coef_curr, rhs)
631 }
632
633 #[test]
634 fn test_topp2_ra_recovery_distribution_stress() {
635 let options = ReachSet2OptionsBuilder::new().build().unwrap();
640 let epsilon = options.lp_feas_tol;
641 let base_cases = [
642 (48.0, 32.0, 4.0),
643 (100.0, 64.0, 4.0),
644 (277.77777777777777, 200.0, 6.0),
645 (6144.0, 4096.0, 8.0),
646 ];
647 let ds_exponents = [-20, 0, 20];
648 let row_scale_exponents = [0, 4, 8];
649 let condition_exponents = [30, 35, 40];
650 let gap_factors = [0.25, 0.75, 1.25, 32.0];
651 let permutations = [
652 [0, 1, 2],
653 [0, 2, 1],
654 [1, 0, 2],
655 [1, 2, 0],
656 [2, 0, 1],
657 [2, 1, 0],
658 ];
659 let mut midpoint_cases = 0usize;
660 let mut masked_nan_cases = 0usize;
661 let mut total_cases = 0usize;
662
663 for &(path_cap, a_prev, tiny_upper_offset) in &base_cases {
664 let robust_upper = path_cap - a_prev;
665 for &ds_exponent in &ds_exponents {
666 let ds = 2.0_f64.powi(ds_exponent);
667 for &row_scale_exponent in &row_scale_exponents {
668 let row_rhs = 300.0 * (a_prev / 64.0) * 2.0_f64.powi(row_scale_exponent);
669 for &condition_exponent in &condition_exponents {
670 let condition = 2.0_f64.powi(condition_exponent);
671 let intended_tiny_upper = robust_upper - tiny_upper_offset;
672 let tiny = row_rhs / (condition * a_prev + intended_tiny_upper);
673 let coef_prev = tiny * condition;
674 let tiny_row = (tiny, coef_prev, row_rhs);
675 let actual_tiny = first_edge_row_after_round_trip(tiny_row, ds);
676 let actual_tiny_upper =
677 (actual_tiny.2 - actual_tiny.1 * a_prev) / actual_tiny.0;
678
679 assert!(
680 actual_tiny_upper.is_finite()
681 && actual_tiny_upper > 0.0
682 && actual_tiny_upper < robust_upper,
683 "bad generated tiny upper: cap={path_cap:e}, prev={a_prev:e}, ds_exp={ds_exponent}, row_scale_exp={row_scale_exponent}, cond_exp={condition_exponent}, upper={actual_tiny_upper:e}"
684 );
685
686 for &gap_factor in &gap_factors {
687 let requested_lower = actual_tiny_upper + gap_factor * epsilon;
688 let canonical_rows = [
689 (1.0, 1.0, path_cap),
690 tiny_row,
691 (-1.0, 1.0, a_prev - requested_lower),
692 ];
693 for (permutation_id, permutation) in permutations.iter().enumerate() {
694 let rows = [
695 canonical_rows[permutation[0]],
696 canonical_rows[permutation[1]],
697 canonical_rows[permutation[2]],
698 ];
699 let constraints =
700 constraints_with_first_edge_rows(&rows, ds, path_cap);
701 let problem = Topp2ProblemBuilder::with_constraint(
702 &constraints,
703 (0, 2),
704 (a_prev, 0.0),
705 )
706 .build()
707 .unwrap();
708 let reach = reach_set2_backward(&problem, &options).unwrap();
709 let mut actual_rows = Vec::new();
710 constraints.fill_acc_topp2::<true>(&mut actual_rows, 0);
711 let lp_tol = LpToleranceOptions::with_feas_tol(epsilon);
712 let reduced_rows =
713 || actual_rows.iter().map(|&(c, d, rhs)| (c, rhs - d * a_prev));
714 let (raw_max, raw_min) = lp_1d::<false>(reduced_rows(), &lp_tol);
715 assert!(
716 raw_max < raw_min,
717 "generated interval is not inverted: cap={path_cap:e}, prev={a_prev:e}, ds_exp={ds_exponent}, row_scale_exp={row_scale_exponent}, cond_exp={condition_exponent}, gap_factor={gap_factor}, perm={permutation_id}, raw=[{raw_min:e},{raw_max:e}]"
718 );
719
720 let (legacy_max, legacy_min) =
725 lp_1d::<true>(reduced_rows(), &lp_tol);
726 let legacy_choice = legacy_max.min(reach.a_max[1]);
727 let legacy_lower = legacy_min.max(reach.a_min[1]);
728 assert!(
729 legacy_choice >= legacy_lower,
730 "legacy fixture did not survive clipping: cap={path_cap:e}, prev={a_prev:e}, ds_exp={ds_exponent}, row_scale_exp={row_scale_exponent}, cond_exp={condition_exponent}, gap_factor={gap_factor}, perm={permutation_id}"
731 );
732 if legacy_max.is_nan() && legacy_min.is_nan() {
733 masked_nan_cases += 1;
734 let gross_residual = legacy_choice + a_prev - path_cap;
735 assert!(
736 gross_residual > 0.5 * a_prev,
737 "legacy NaN masking was not grossly infeasible: residual={gross_residual:e}"
738 );
739 } else {
740 midpoint_cases += 1;
741 assert_eq!(legacy_max, legacy_min);
742 assert!(
743 robust_upper - legacy_choice > 0.5 * tiny_upper_offset,
744 "legacy midpoint was not observably suboptimal: legacy={legacy_choice:e}, robust_upper={robust_upper:e}"
745 );
746 }
747
748 let profile = topp2_ra(&problem, &options).unwrap_or_else(|error| {
749 panic!(
750 "new solver failed: cap={path_cap:e}, prev={a_prev:e}, ds_exp={ds_exponent}, row_scale_exp={row_scale_exponent}, cond_exp={condition_exponent}, gap_factor={gap_factor}, perm={permutation_id}: {error:?}"
751 )
752 });
753 assert_eq!(profile.len(), 3);
754 assert_eq!(profile[0], a_prev);
755 assert_eq!(profile[2], 0.0);
756
757 let oracle = robust_upper + 2.0_f64.sqrt() * epsilon;
762 let oracle_roundoff = 512.0 * f64::EPSILON * oracle.abs().max(1.0);
763 assert!(
764 (profile[1] - oracle).abs() <= oracle_roundoff,
765 "not the largest certified state: got={:e}, oracle={oracle:e}, allowance={oracle_roundoff:e}, cap={path_cap:e}, prev={a_prev:e}, ds_exp={ds_exponent}, row_scale_exp={row_scale_exponent}, cond_exp={condition_exponent}, gap_factor={gap_factor}, perm={permutation_id}",
766 profile[1]
767 );
768
769 for &(coef_curr, coef_prev, rhs) in &actual_rows {
770 let raw = coef_curr * profile[1] + coef_prev * profile[0] - rhs;
771 let norm = coef_curr.hypot(coef_prev);
772 let normalized = raw / norm;
773 let normalized_second_order = (coef_curr / norm) * profile[1]
774 + (coef_prev / norm) * profile[0]
775 - rhs / norm;
776 assert!(
777 normalized <= epsilon
778 && normalized_second_order <= epsilon
779 && raw <= epsilon * rhs.abs().max(1.0),
780 "uncertified row: row=({coef_curr:e},{coef_prev:e},{rhs:e}), raw={raw:e}, norm1={normalized:e}, norm2={normalized_second_order:e}, profile={:?}",
781 profile
782 );
783 }
784 let (path_exceed, physical_exceed) =
785 constraints.exceed_topp2(0, &profile);
786 assert!(path_exceed <= 0.0);
787 assert!(
788 physical_exceed <= 1.0e-5,
789 "physical audit failed: exceed={physical_exceed:e}, profile={profile:?}, cap={path_cap:e}, prev={a_prev:e}, ds_exp={ds_exponent}, row_scale_exp={row_scale_exponent}, cond_exp={condition_exponent}, gap_factor={gap_factor}, perm={permutation_id}"
790 );
791 total_cases += 1;
792 }
793 }
794 }
795 }
796 }
797 }
798
799 assert_eq!(total_cases, 2592);
800 assert_eq!(midpoint_cases, 1296);
801 assert_eq!(masked_nan_cases, 1296);
802 }
803
804 #[test]
805 #[ignore = "bindings"]
806 fn test_topp2_ra_bindings_parity() -> Result<(), CoppError> {
807 let dim = 3;
808 let num_waypoints = 8;
809 let n: usize = 201;
810 let pi = std::f64::consts::PI;
811
812 let waypoints = DMatrix::<f64>::from_fn(dim, num_waypoints, |axis, j| {
813 let s = j as f64 / (num_waypoints - 1) as f64;
814 match axis {
815 0 => 0.20 * (2.0 * pi * s).sin(),
816 1 => 0.15 * (1.5 * pi * s).cos(),
817 2 => 0.10 * s * (1.0 - s),
818 _ => unreachable!("dimension is fixed to 3"),
819 }
820 });
821 let path = Path::from_waypoints_interpolating(&waypoints, SplineConfig::default())?;
822 let s = DMatrix::<f64>::from_fn(1, n, |_, j| j as f64 / (n - 1) as f64);
823
824 let mut robot = Robot::with_capacity(dim, n);
825 robot
826 .with_s(&s.as_view())?
827 .with_q_from_path_2nd(&path, 0, n)?
828 .with_q_from_path_3rd(&path, 0, n)?;
829 add_symmetric_axial_limits_for_test(&mut robot, 10.0, 50.0, Some(1000.0))?;
830
831 let topp2_problem = Topp2ProblemBuilder::new(&robot, (0, n - 1), (0.0, 0.0)).build()?;
832 let options = ReachSet2OptionsBuilder::new().build()?;
833 let a_profile = topp2_ra(&topp2_problem, &options)?;
834
835 let (t_final, t_s) = s_to_t_topp2(s.as_slice(), &a_profile, 0.0)?;
836 let s_t = t_to_s_topp2(
837 s.as_slice(),
838 &a_profile,
839 &t_s,
840 InterpolationMode::UniformTimeGrid(0.0, 1e-3, true),
841 )?;
842
843 crate::verbosity_log!(
844 Verbosity::Summary,
845 "TOPP2-RA Rust bindings parity test: t_final={:.17}, s_t.len={}",
846 t_final,
847 s_t.len()
848 );
849
850 Ok(())
851 }
852
853 #[test]
856 #[ignore = "slow"]
857 fn test_topp2_ra_robust() -> Result<(), CoppError> {
858 run_test_topp2_ra_repeated(10000, true)
859 }
860
861 fn run_test_topp2_ra_repeated(n_exp: usize, flag_print_step: bool) -> Result<(), CoppError> {
862 let mut tc_sum_ra = Duration::ZERO;
863 let mut tc_sum_interpolation = Duration::ZERO;
864 let mut t_final_sum = 0.0;
865
866 let options = ReachSet2OptionsBuilder::new()
867 .lp_feas_tol(1E-9)
868 .a_cmp_abs_tol(1E-9)
869 .a_cmp_rel_tol(1E-9)
870 .verbosity(Verbosity::Summary)
871 .build()?;
872
873 for i_exp in 0..n_exp {
874 let dim = 7;
875 let n: usize = 1000;
876 let mut robot = Robot::with_capacity(dim, n);
877
878 let mut rng = rand::rng();
879 let (s, path, _, _) = lissajous_path_for_test(dim, n, &mut rng).map_err(|e| {
880 CoppError::InvalidInput("lissajous_path_for_test".into(), e.to_string())
881 })?;
882 robot
883 .with_s(&s.as_view())?
884 .with_q_from_path_2nd(&path, 0, n)?;
885 add_symmetric_axial_limits_for_test(&mut robot, 1.0, 1.0, None)?;
886
887 let start = Instant::now();
888 let topp2_problem = Topp2ProblemBuilder::new(&robot, (0, n - 1), (0.0, 0.0)).build()?;
889 let a_profile = topp2_ra(&topp2_problem, &options)?;
890 let tc_topp2_ra = start.elapsed();
891
892 let b_profile = a_to_b_topp2(s.as_slice(), &a_profile)?;
893 assert!(
894 !izip!(
895 s.as_slice().windows(2),
896 a_profile.windows(2),
897 b_profile.iter()
898 )
899 .any(|(s_pair, a_pair, b)| {
900 let ds_double = 2.0 * (s_pair[1] - s_pair[0]);
901 let db = (a_pair[1] - a_pair[0]) / ds_double;
902 (*b - db).abs() > 1e-3
903 }),
904 "b_profile generation failed!"
905 );
906
907 let start = Instant::now();
908 let (t_final, t_s) = s_to_t_topp2(s.as_slice(), &a_profile, 0.0)?;
909 assert_eq!(t_s.len(), s.ncols());
910 let tc_interpolation = start.elapsed();
911 let s_t = t_to_s_topp2(
912 s.as_slice(),
913 &a_profile,
914 &t_s,
915 InterpolationMode::UniformTimeGrid(0.0, 1E-3, true),
916 )?;
917
918 tc_sum_ra += tc_topp2_ra;
919 tc_sum_interpolation += tc_interpolation;
920 t_final_sum += t_final;
921
922 if flag_print_step && ((i_exp + 1) % 100 == 0) {
923 crate::verbosity_log!(
924 Verbosity::Summary,
925 "Exp #{}: tc_topp2_ra = {:.4} ms, tc_interpolation = {:.4} ms, t_final = {:.4} s, s_t.len() = {}",
926 i_exp + 1,
927 tc_topp2_ra.as_secs_f64() * 1E3,
928 tc_interpolation.as_secs_f64() * 1E3,
929 t_final,
930 s_t.len()
931 );
932 }
933 }
934
935 crate::verbosity_log!(
936 Verbosity::Summary,
937 "Average over {} experiments: tc_topp2_ra = {:.6} ms, tc_interpolation = {:.6} ms, t_final = {:.6}",
938 n_exp,
939 tc_sum_ra.as_secs_f64() * 1E3 / n_exp as f64,
940 tc_sum_interpolation.as_secs_f64() * 1E3 / n_exp as f64,
941 t_final_sum / n_exp as f64
942 );
943
944 Ok(())
945 }
946}